課程:JavaScript 與 React 底層原理 第 9 堂:ES6+ 語法收尾
29 函數式陣列操作
想像你正在開發一個電商網站的後台。API 回傳了一個包含 500 個訂單的陣列,你需要完成以下任務:首先,篩選出今天成交的訂單;接著,從中提取每個訂單的總金額;最後,計算出今天的總營收。
如果你使用傳統的 for 迴圈,你可能需要建立好幾個暫存變數,寫下複雜的索引邏輯,還要小心翼翼地處理迴圈邊界。但如果我告訴你,這一切可以用一種像「生產線流水線」一樣優雅、且幾乎不會出錯的方式完成呢?
在 JavaScript 的世界裡,這就是 函數式陣列操作 的魅力所在。這不僅是現代 JS 開發的核心,更是 React 渲染列表、處理狀態(State)時唯一推薦的標準做法。
從命令式到宣告式:思維的轉向
在深入具體的語法之前,我們必須先理解一種程式設計哲學的轉變:從 命令式(Imperative) 轉向 宣告式(Declarative)。
命令式:告訴電腦「怎麼做」
傳統的 for 迴圈是命令式的典型代表。你必須詳細交代每一個步驟:
- 建立一個空的結果陣列。
- 宣告一個索引
i從 0 開始。 - 判斷
i是否小於陣列長度。 - 取得目前元素並判斷條件。
- 將符合條件的元素推入結果陣列。
- 將
i加 1 並重複。
這種寫法的問題在於,程式碼充斥著控制邏輯(如何遍歷、如何管理索引),而真正的業務邏輯(篩選與轉換)卻被淹沒在其中。
宣告式:告訴電腦「我要什麼」
函數式陣列操作(如 map、filter)則是宣告式的。你不再關心迴圈怎麼跑、索引怎麼算,你只需要傳達你的意圖:
- 「我要篩選出金額大於 100 的項目。」
- 「我要將每個項目轉換成 HTML 標籤。」
這就是為什麼在 React 中你幾乎看不到 for 迴圈,因為 React 本身就是宣告式 UI 框架。我們描述 UI 「應該長什麼樣子」,而不是命令瀏覽器「如何一步步修改 DOM」。
不可變性(Immutability):React 的生命線
在使用 map、filter 和 reduce 之前,你必須牢記一個最高準則:這些方法永遠不會修改原始陣列。
它們會執行邏輯,然後回傳一個全新的陣列(或是 reduce 產生的新值)。這在 JS 中被稱為「無副作用(Side-effect free)」的操作。
為什麼這對 React 如此重要?
React 的渲染機制高度依賴「參考比較(Reference Equality)」。當你呼叫 setItems(newItems) 時,React 會快速檢查 newItems 的記憶體位址是否與舊的 items 不同。如果不同,它就知道資料變了,該重新渲染。
如果你使用 push 或 splice 直接修改原始陣列,記憶體位址沒變,React 可能會認為資料沒有更新,進而導致畫面不觸發重繪的靈異現象。因此,掌握這些回傳新陣列的方法,是寫好 React 的基本功。
map:資料的 1 對 1 轉換
map 是 React 開發者最親密的朋友。它的核心功能是:將陣列中的每一個元素,按照你提供的函數邏輯,「映射」成另一個新的元素。
運作邏輯
- 輸入:長度為 N 的陣列。
- 輸出:長度同樣為 N 的新陣列。
- 特性:它是一對一的,輸入幾個,產出就是幾個。
範例:從資料到 UI 的橋樑
假設我們有一組使用者資料:
const users = [
{ id: 1, name: 'Aria', active: true },
{ id: 2, name: 'Bob', active: false },
{ id: 3, name: 'Charlie', active: true }
];
// 我們想將這組物件,轉換成 React 渲染需要的標籤(這裡用字串模擬)
const userElements = users.map((user) => {
return `<li>${user.name}</li>`;
});
console.log(userElements);
// 產出:["<li>Aria</li>", "<li>Bob</li>", "<li>Charlie</li>"]
在 React 中的實戰預覽
在 JSX 中,我們直接在大括號內呼叫 map 來生成列表:
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
注意: 這裡我們看到了 key 屬性。這是 React 的一個優化機制。雖然我們在 Topic 7 才會深入探討 Reconciliation,但現在請記住:當你用 map 產出元件列表時,必須給每個元素一個唯一的 key,這樣 React 才能在資料變動時,精確地知道哪一個 DOM 節點需要更新,而不是全部砍掉重練。
filter:資料的守門員
filter 的工作非常單純:根據你提供的條件,決定哪些元素可以留下來,哪些該被剔除。
運作邏輯
- 輸入:長度為 N 的陣列。
- 回傳:一個布林值(
true代表保留,false代表捨棄)。 - 輸出:一個長度小於或等於 N 的新陣列。
範例:篩選活躍使用者
延續剛才的 users 例子,如果我們只想顯示正在線上的使用者:
const activeUsers = users.filter((user) => {
// 只有當 user.active 為 true 時,這個元素才會出現在新陣列中
return user.active;
});
console.log(activeUsers);
// 產出:兩個物件的陣列(Aria 和 Charlie)
思考題:如果沒有人符合條件會怎樣?
如果你執行 filter 但沒有任何元素符合條件,它會回傳一個空的陣列 [],而不是 null 或 undefined。這在 React 中非常安全,因為 [].map(...) 不會報錯,只會單純地不渲染任何東西,避免了程式當掉的風險。
reduce:陣列操作的萬用瑞士刀
如果說 map 是轉換、filter 是挑選,那麼 reduce 就是「折疊」。它能將整個陣列濃縮成一個單一的值。這個值可以是數字、字串、一個物件,甚至可以是另一個完全不同結構的陣列。
reduce 是初學者最容易卡關的地方,但只要掌握了它的「累加器」概念,你就能處理最複雜的資料結構。
核心術語
reduce 接受兩個參數:
- Callback 函數:定義如何處理每個元素。它接收兩個重要參數:
accumulator(acc): 累加器,它是上一次函數執行的結果。currentValue(cur): 目前正在處理的陣列元素。
- 初始值 (Initial Value):這是
accumulator的起點。強烈建議永遠提供初始值。
範例:計算總積分
const scores = [10, 20, 30, 40];
// 我們想計算總分
const totalScore = scores.reduce((acc, cur) => {
console.log(`目前累加:${acc}, 目前數值:${cur}`);
return acc + cur;
}, 0); // 這裡的 0 就是初始值
console.log(totalScore); // 100
進階應用:將陣列轉換為物件
在前端開發中,我們常需要將陣列轉成「以 ID 為 Key」的物件,以便快速查找。這正是 reduce 的拿手好戲:
const userMap = users.reduce((acc, user) => {
// 將 user.id 作為 key,user 物件本身作為 value
acc[user.id] = user;
return acc;
}, {}); // 初始值是一個空物件
console.log(userMap[1]); // { id: 1, name: 'Aria', ... }
為什麼 **reduce** 被稱為瑞士刀?
因為理論上,你可以用 reduce 實作出 map 和 filter 的所有功能。它是陣列方法中最底層、最強大的工具。當你發現 map 和 filter 組合起來仍難以處理某些複雜邏輯時(例如:需要在一次遍歷中同時完成篩選與計算),reduce 就是你的終極解法。
鏈式呼叫(Chaining):建立資料處理管線
這就是函數式編程最美妙的時刻。因為 map 和 filter 都回傳陣列,我們可以像接水管一樣,將多個操作串連在一起。
實戰場景:從原始資料到最終統計
假設我們拿到一份原始訂單資料,我們要執行以下動作:
- 只看「已完成」的訂單(
filter)。 - 提取這些訂單的「金額」(
map)。 - 計算「總營收」(
reduce)。
const orders = [
{ id: 101, status: 'completed', amount: 500 },
{ id: 102, status: 'pending', amount: 1000 },
{ id: 103, status: 'completed', amount: 250 },
{ id: 104, status: 'cancelled', amount: 300 }
];
const totalRevenue = orders
.filter(order => order.status === 'completed') // 先過濾
.map(order => order.amount) // 再轉型
.reduce((total, price) => total + price, 0); // 最後總計
console.log(totalRevenue); // 750
這段程式碼讀起來幾乎就像英文句子一樣自然:「從訂單中,過濾出完成的,拿出金額,然後加總。」比起寫三個 for 迴圈並手動維護中間變數,這種寫法極大地降低了認知負擔,也更容易除錯。
總結:React 中的陣列操作思維
在 React 專案中,你會不斷重複以下模式:
- 獲取資料:從 API 拿回一個陣列。
- 準備資料:使用
filter排除不需要的、使用map格式化日期或金額。 - 渲染資料:在 JSX 中使用
map將處理後的資料轉換為元件,並記得補上key。
掌握了這三個陣列方法,你就不再只是在「寫程式」,而是在「引導資料流動」。
核心複習:
- map: 1 對 1 轉換,長度不變。
- filter: 篩選,長度變小或不變。
- reduce: 折疊成單一結果(數值、物件等)。
- 不可變性: 它們都回傳新東西,這對 React 的狀態更新至關重要。
Topic 5 知識點回顧與整合
恭喜你!我們已經完成了 Topic 5: ES6+ 現代語法精要 的所有核心教學。
在這個主題中,我們從最基礎的解構賦值出發,學會了如何優雅地拆解 Props 與 State;接著深入探討了 Spread 與 Rest 運算子,理解了「淺複製」在不可變狀態更新中的重要性。我們也拆解了箭頭函數的 Lexical This 特性,這讓我們在 React 事件處理器中不再迷失方向。隨後,我們認識了 ES 模組系統 的靜態結構如何幫助現代工具進行效能優化(Tree Shaking),並學會了用 ?. (Optional Chaining) 與 ?? (Nullish Coalescing) 來防禦性地處理複雜的 API 資料。最後,我們透過本節的函數式陣列操作,將這一切知識串連起來,完成了從原始資料到 UI 呈現的最後一哩路。
這些語法不僅僅是為了讓程式碼變短,它們背後蘊含的「宣告式」、「不可變性」與「詞法作用域」思維,正是 React 框架設計的靈魂所在。掌握了這些,你已經具備了進入 React 核心原始碼與複雜架構的紮實基礎。
下一堂課,我們將正式跨入 Topic 6:React 核心設計哲學。我們將探討為什麼 React 選擇了這條路,Virtual DOM 的真面目是什麼,以及我們寫下的 JSX 程式碼到底是如何被轉換成瀏覽器看得懂的畫面的。